[TOC]
1 一些必要的前提
1.1 面向过程
在计算机发展的早期阶段,所有的与计算机相关的操作都是以计算机为中心的(迁就计算机),比如穿孔纸带记录信息,机器码编程,汇编语言编程等。
受早期计算机文化的影响,后来的很多编程语言的设计和实现、以及相关的软件开发过程都与以计算机为中心有关。比如大名鼎鼎的C语言,结构化的设计和编码。
1.2 面向对象
- 以人为本
我们擅长于对现实世界的事物进行抽象归类,比如好人坏人,男人女人,高富帅,白富美等,也能很容易的道出各种对象的属性和操作,比如有钱、有权、有地位,比如开豪车,泡美眉等。 - 代码复用
通过分析归纳,根据业务将类型及其关系抽象出来,通过继承的方式复用代码,减少冗余。
1.3 区别和联系
- 不同的侧重点,面相过程侧重于过程,面向对象侧重于对象(废话,顾名思义)
- 不同视角的思维方式,面向过程以执行流程为导向,面向对象以对象交互为导向。
- 不同的抽象程度,面向过程着眼于具体流程,抽象程度低;面向对象可以更高级别的抽象。
- 总是同时出现,面向对象的操作就是一个个的过程,面向过程的作用目标就是对象。
一个例子,参考:
- 面向对象: 狗.吃(屎)
- 面向过程: 吃.(狗,屎)
看完了你好像知道该怎么选了,^_^。狗,屎都是对象,吃是一个过程,二者是难舍难分啊,所以面向对象和面向过程是无法一刀切开的。
面向过程更关注吃这个过程(狗和屎都是依赖),面向对象更关注狗这个对象,吃只是操作,屎是操作的数据。
2 JavaScript 面向对象编程
2.1 原型(prototype)
JavaScript通过原型属性来实现继承,几个概念揉在了一起,咱们撕开来看看:
- 函数原型 FuncObj.prototype 对象
函数对象(一切皆是对象,函数也不例外)的原型对象(是一个对象,一个对象) - 对象原型 obj.proto 对象
对象引用的构造器提供的原型,在ECMAScript规范中使用[[Prototype]]内部属性来表达,在IE中不知道是什么,在W3C浏览器中使用__proto__属性来表达,可直接访问。
在ECMAScript5之前是非标准属性,在ECMAScript2015(ES6)中__proto__被标准化为一个对象的原型属性。 - 函数prototype 属性
- 对象内部[[Prototype]]属性
- 以上都叫原型
2.2 继承和重写
- 继承就是要复用现有的代码
- 可以通过原型的方式实现共享
- 可以通过浅拷贝的方式进行共享
- 通过深拷贝的方式实现重写
- 有了上面这几条就可以变出各种姿势,如jQuery的深浅拷贝,ExtJS的原型继承+浅拷贝等
2.3 原型继承
JavaScript特有的原型继承,就是基于上面的原型。
使用过程如下:
定义一个带原型的构造函数
function Person(config) { config = config || {}; this.name = config.name || 'defaultName'; } Person.prototype = { say: function () { console.log(this.name, 'say something') } }
通过new Person 生成对象
var jim = new Person({ name:'jim' }); jim.say(); var john = new Person({ name:'john' }); john.say();
jim 和 john这两个对象共享了Person.prototype对象中的say(内存中就一份)
new Person的执行过程
- 创建一个空对象,并把this执行这个对象
- 将对象的[[Prototype]]属性(proto)设置为Person.prototype
- 然后执行Person构造函数中的代码
等价的代码:
一种姿势
var jim = new Person({ name: 'jim' })
另一种姿势
var jim = {}; Person.call(jim,{ name:'jim' })
执行的查找过程
var john = new Person({ name:'john' }); john.say();
调用john.say()时,编译器先查找john本身是否有say这个属性,发现没有;于是查找john.__proto__这个对象里面是否包含say这个属性,结果找到了就做一些检测,比如说是不是函数啊等,然后执行得到结果。
当然调用的this始终是当前的对象。上面的查找过程,就是愚公移山的过程,子又生孙,孙又生子。。。。
2.4 多级继承
2.4.1 一个多级继承的实例
function Man(config){
this.sex = 'man'
Person.call(this,config);
}
Man.prototype = Person.prototype;
function Woman(config){
this.sex = 'woman'
Person.call(this,config)
}
Woman.prototype = Person.prototype;
var man = new Man({
name:'testMan'
});
var woman = new Woman({
name:'testWoman'
});
man.say();
woman.say();
2.4.2 需要修改的原型对象
如果我们需要修改Man和Women的原型对象,分别加入Man和Women各自特有的属性和方法,那么上面的代码就不太适用,有如下问题:
- 修改Man或Woman的原型都影响到了Woman或Man的原型
- 修改Women和Man的原型影响到了Person的原型
- 上面两句都是废话,因为他们三个引用的是同一个对象
可以用如下方法解决:
Man.prototype = new Person();
Woman.prototype = new Person();
但这样之后:
- 在查找say方法的过程中多了一次查找到person对象的过程,然后才找到Person.prototype,原型链多了一个层级
- 在man 或woman对象中有一个name属性,在person中还有一个多余的name属性
我们可以直接用一个空函数来代替作为衔接的构造函数Person,但Person构造函数还会被借用(作为基类构造函数),不能直接干掉。
function TmpConstructor(){}
TmpConstructor.prototype = Person.prototype;
Man.prototype = new TmpConstructor();
Woman.prototype = new TmpConstructor();
这样就解决了冗余熟悉的问题,但多一层原型链,在需要修改原型的需求中不能被干掉,干掉就回到上一步了,满足不了需求。
2.4.3 需要重写的原型属性
如果是完全重写的属性或方法,直接在子类的原型中修改就行,这样就会在原型链查找的过程中出现短路的效果,从而无视原型中的同名属性。
但如果只是想在原来的基础上做一些小的改动,增加特殊的操作,完全重写一遍总不是我们想要的,JavaScript没有提供直接访问父类的原型的途径(ES6中可以使用__proto__)
还是方法借用,一个简单的方式是直接使用父类原型来访问。这个跟其他像Java这样的面向对象语言的super关键字不一样,只是单纯的方法借用,跟继承关系没有关联。
function Person(config) {
config = config || {};
this.name = config.name || 'defaultName';
}
Person.prototype = {
say: function () {
console.log(this.name, 'say something')
}
}
function Man(config){
this.sex = 'man'
Person.call(this,config);
}
function Woman(config){
this.sex = 'woman'
Person.call(this,config)
}
function TmpConstructor(){}
TmpConstructor.prototype = Person.prototype;
Man.prototype = new TmpConstructor();
Man.prototype.say = function(){
console.log('a man:')
TmpConstructor.prototype.say.call(this);
}
Woman.prototype = new TmpConstructor();
Woman.prototype.say = function(){
console.log('a woman:')
TmpConstructor.prototype.say.call(this);
}
var man = new Man({
name:'testMan'
});
var woman = new Woman({
name:'testWoman'
});
man.say();
woman.say();
2.5 无招胜有招
JavaScript的面向对象实现方式有些局限性,对面向对象的支持还在不断的完善,ES6中会直接提供相关的语法支持。
然而对信息隐藏的支持还是有限,比如想提供一个私有变量,直接读取:
function Person(config){
config = config || {};
var secret = config.secret || 'mysecret';
this.getSecret = function(){
return secret;
}
}
这样secret作为一个私有变量,只能通过getSecret方法来访问,但这个访问方法每个对象会保存一份。
当然我们的目标是封装和代码复用,有了上面的道道,要用什么姿势,随你~
3 参考资料
最后更新: 2022年03月02日 03:32
原始链接: http://rawbin-.github.io/architecture/2015-09-12-javascript-oop/